/*
 * This file is part of Applied Energistics 2. Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. Applied
 * Energistics 2 is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version. Applied Energistics 2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
 * Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
 * Applied Energistics 2. If not, see <http://www.gnu.org/licenses/lgpl>.
 */

package appeng.recipes.loader;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;

import org.apache.commons.io.FileUtils;

import com.google.common.base.Preconditions;

/**
 * copies recipes in jars onto file system includes the readme, needs to be modified if other files needs to be handled
 *
 * @author thatsIch
 * @version rv3 - 11.05.2015
 * @since rv3 11.05.2015
 */
public class RecipeResourceCopier {

    /**
     * Most expected size of recipes found
     */
    private static final int INITIAL_RESOURCE_CAPACITY = 20;

    private static final Pattern DOT_COMPILE_PATTERN = Pattern.compile(".", Pattern.LITERAL);
    private static final String FILE_PROTOCOL = "file";
    private static final String CLASS_EXTENSION = ".class";
    private static final String JAR_PROTOCOL = "jar";
    private static final String UTF_8_ENCODING = "UTF-8";

    /**
     * copy source in the jar
     */
    private final String root;

    /**
     * @param root source root folder of the recipes inside the jar.
     * @throws NullPointerException if root is <tt>null</tt>
     */
    public RecipeResourceCopier(@Nonnull final String root) {
        Preconditions.checkNotNull(root);

        this.root = root;
    }

    /**
     * copies recipes found in the root to destination.
     *
     * @param identifier  only copy files which end with the identifier
     * @param destination destination folder to which the recipes are copied to
     * @throws URISyntaxException       {@see #getResourceListing}
     * @throws IOException              {@see #getResourceListing} and if copying the detected resource to file is not
     *                                  possible
     * @throws NullPointerException     if either parameter is <tt>null</tt>
     * @throws IllegalArgumentException if destination is not a directory
     */
    public void copyTo(@Nonnull final String identifier, @Nonnull final File destination)
            throws URISyntaxException, IOException {
        Preconditions.checkNotNull(destination);
        Preconditions.checkArgument(destination.isDirectory());

        this.copyTo(identifier, destination, this.root);
    }

    /**
     * @param destination destination folder to which the recipes are copied to
     * @param directory   the folder to copy.
     * @throws URISyntaxException {@see #getResourceListing}
     * @throws IOException        {@see #getResourceListing} and if copying the detected resource to file is not
     *                            possible
     * @see {RecipeResourceCopier#copyTo(File)}
     */
    private void copyTo(@Nonnull final String identifier, @Nonnull final File destination,
            @Nonnull final String directory) throws URISyntaxException, IOException {
        assert identifier != null;
        assert destination != null;
        assert directory != null;

        final Class<? extends RecipeResourceCopier> copierClass = this.getClass();
        final String[] listing = this.getResourceListing(copierClass, directory);
        for (final String list : listing) {
            if (list.endsWith(identifier)) {
                // generate folder before the file is copied so no empty folders will be generated
                FileUtils.forceMkdir(destination);

                this.copyFile(destination, directory, list);
            } else if (!list.contains(".")) {
                final File subDirectory = new File(destination, list);

                this.copyTo(identifier, subDirectory, directory + list + "/");
            }
        }
    }

    /**
     * Copies a single file inside a folder to the destination.
     *
     * @param destination folder to which the file is copied to
     * @param directory   the directory containing the file
     * @param fileName    the file to copy
     * @throws IOException if copying the file is not possible
     */
    private void copyFile(@Nonnull final File destination, @Nonnull final String directory,
            @Nonnull final String fileName) throws IOException {
        assert destination != null;
        assert directory != null;
        assert fileName != null;

        final Class<? extends RecipeResourceCopier> copierClass = this.getClass();
        final InputStream inStream = copierClass.getResourceAsStream('/' + directory + fileName);
        final File outFile = new File(destination, fileName);

        if (!outFile.exists() && inStream != null) {
            FileUtils.copyInputStreamToFile(inStream, outFile);
            inStream.close();
        }
    }

    /**
     * List directory contents for a resource folder. Not recursive. This is basically a brute-force implementation.
     * Works for regular files and also JARs.
     *
     * @param clazz Any java class that lives in the same place as the resources you want.
     * @param path  Should end with "/", but not start with one.
     * @return Just the name of each member item, not the full paths.
     * @throws URISyntaxException            if it is a file path and the URL can not be converted to URI
     * @throws IOException                   if jar path can not be decoded
     * @throws UnsupportedOperationException if it is neither in jar nor in file path
     */
    @Nonnull
    private String[] getResourceListing(@Nonnull final Class<?> clazz, @Nonnull final String path)
            throws URISyntaxException, IOException {
        assert clazz != null;
        assert path != null;

        final ClassLoader classLoader = clazz.getClassLoader();
        if (classLoader == null) {
            throw new IllegalStateException(
                    "ClassLoader was not found. It was probably loaded at a inappropriate time");
        }

        URL dirURL = classLoader.getResource(path);
        if (dirURL != null) {
            final String protocol = dirURL.getProtocol();
            if (protocol.equals(FILE_PROTOCOL)) {
                // A file path: easy enough

                final URI uriOfURL = dirURL.toURI();
                final File fileOfURI = new File(uriOfURL);
                final String[] filesAndDirectoriesOfURI = fileOfURI.list();

                if (filesAndDirectoriesOfURI == null) {
                    throw new IllegalStateException(
                            "Files and Directories were illegal. Either an abstract pathname does not denote a directory, or an I/O error occured.");
                } else {
                    return filesAndDirectoriesOfURI;
                }
            }
        }

        if (dirURL == null) {
            /*
             * In case of a jar file, we can't actually find a directory. Have to assume the same jar as clazz.
             */
            final String className = clazz.getName();
            final Matcher matcher = DOT_COMPILE_PATTERN.matcher(className);
            final String me = matcher.replaceAll("/") + CLASS_EXTENSION;
            dirURL = classLoader.getResource(me);
        }

        if (dirURL != null) {
            final String protocol = dirURL.getProtocol();
            if (protocol.equals(JAR_PROTOCOL)) {
                /* A JAR path */
                final String dirPath = dirURL.getPath();
                final String jarPath = dirPath.substring(5, dirPath.indexOf('!')); // strip out only
                // the JAR file
                try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, UTF_8_ENCODING))) {
                    final Enumeration<JarEntry> entries = jar.entries(); // gives ALL entries in jar
                    final Collection<String> result = new HashSet<>(INITIAL_RESOURCE_CAPACITY); // avoid
                    // duplicates

                    // in case it is a
                    // subdirectory
                    while (entries.hasMoreElements()) {
                        final JarEntry entry = entries.nextElement();
                        final String entryFullName = entry.getName();
                        if (entryFullName.startsWith(path)) { // filter according to the path
                            String entryName = entryFullName.substring(path.length());
                            final int checkSubDir = entryName.indexOf('/');
                            if (checkSubDir >= 0) {
                                // if it is a subdirectory, we just return the directory name
                                entryName = entryName.substring(0, checkSubDir);
                            }
                            result.add(entryName);
                        }
                    }

                    return result.toArray(new String[result.size()]);
                }
            }
        }

        throw new UnsupportedOperationException("Cannot list files for URL " + dirURL);
    }
}
